/* Copyright   1991 Gustavus Adolphus College.  All rights reserved.
 *
 * Schematik was developed by Gustavus Adolphus College (GAC) with
 * support from NeXT Computer, Inc.  Permission to copy this software,
 * to redistribute it, and to use it for any purpose is granted,
 * subject to the following restrictions and understandings.
 *
 * 1. Any copy made of this software must include this copyright
 * notice in full.
 *
 * 2. Users of this software agree to make their best efforts (a) to
 * return to the GAC Mathematics and Computer Science Department any
 * improvements or extensions that they make, so that these may be
 * included in future releases; and (b) to inform GAC of noteworthy
 * uses of this software.
 *
 * 3. All materials developed as a consequence of the use of this
 * software shall duly acknowledge such use, in accordance with the
 * usual standards of acknowledging credit in academic research.
 *
 * 4. GAC makes no express or implied warranty or representation of
 * any kind with respect to this software, including any warranty
 * that the operation of this software will be error-free.  ANY
 * IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR
 * PURPOSE IS HEREBY DISCLAIMED.  GAC is under no obligation to
 * provide any services, by way of maintenance, update, or otherwise.
 *
 * 5. In conjunction with products arising from the use of this
 * material, there shall be no use of the name of Gustavus Adolphus
 * College nor of any adaptation thereof in any advertising,
 * promotional, or sales literature without prior written consent
 * from GAC in each case.
 */

#import "DocText.h"
#import "../defines.h"
#import DocWin_h
#import FindAgent_h
#import InteractionWin_h
#import PrefAgent_h
#import <appkit/Application.h>
#import <appkit/Font.h>
#import <appkit/Matrix.h>
#import <appkit/Menu.h>
#import <appkit/NXCursor.h>
#import <appkit/NXImage.h>
#import <appkit/ScrollView.h>
#import <dpsclient/event.h>
#import <sys/types.h>
#import <nextdev/keycodes.h>
#import <objc/List.h>
#import <NXCType.h>
#import <string.h>
#import <streams/streams.h>
#import <ctype.h>
extern void usleep(unsigned);
extern void NXBeep(void);
extern void *malloc(), *realloc(), free();

#define LEFT_PAREN		'('
#define RIGHT_PAREN		')'
#define LEFT_BRACKET		'['
#define RIGHT_BRACKET		']'
#define LEFT_BRACE		'{'
#define RIGHT_BRACE		'}'
#define LEFT_PARENS		"([{"
#define RIGHT_PARENS		")]}"
#define PBLINKDELAY		175000
#define COMMENTSTRING		";"
#define COMMENTCHAR		';'
#define BADLINEBEGINSET		" \n\t\r\v\f)]};"
#define DELIMETERSET		" \n\t\r\v\f();\"\'`|[]{}\377"
#define PREFIXCHARS		"\'`,@#"
#define BIG_INDENT		4
#define SMALL_INDENT		2
#define MAXTOK			1024
#define MAX_FWD_ONLY		4
#define TABWIDTH		8
#define PAREN_NO_MATCH		-1
#define NOT_REALLY_PAREN	-2

#define larrow	172
#define uarrow	173
#define rarrow	174
#define darrow	175

typedef struct cntxt {
  struct cntxt *next;
  int elts;
  int distinguishedElts;
  int startPosition;
  int alignmentPosition;
  BOOL isLet;
} Context;

static BOOL inSet(const char, const char *);
static int findParensMatch(id, const char, int, char *);
static int findBeginningOfScope(id, int, char *);
static int skipspacecommentnewline(const char[], int, int);
static int skipstring(const char[], int, int);
static int findEndOfScope(id, int, char *);
static int getcolumnofposition(id, int);
static void newElt(Context *, BOOL, NXStream *);

static char *selectionBuffer=NULL;

BOOL inOtherMethod=NO;
int last1ClickPos=0;

@implementation DocText

#define PRE_CHANGE	NXSelPt start,end; int tlength=0,i; BOOL wasInOtherMethod=inOtherMethod;	\
			if (!wasInOtherMethod)								\
			  { inOtherMethod=YES; [super getSel:&start :&end]; tlength=textLength;	}

#define POST_CHANGE(POSITION,FROM,TO)	\
  if (!wasInOtherMethod)		\
    { inOtherMethod=NO;			\
      for(i=[markerList count]-1; i; i--) \
        [[markerList objectAt:i] adjustTo:(POSITION) forPositionsFrom:(FROM) to:(TO) andAfterBy:(textLength-tlength)];	\
      if ([window isMemberOf:[InteractionWin class]])									\
        [[markerList objectAt:0] adjustTo:(POSITION) forPositionsFrom:(FROM) to:(TO) andAfterBy:(textLength-tlength)];	\
      [indentationState changeAt:FROM];}

- initFrame:(NXRect *)r
{
    return [self initFrame:r text:"" alignment:NX_LEFTALIGNED];
}

- initFrame:(NXRect *)r text:(const char *)text alignment:(int)mode
{
    char *font=[[PrefAgent new] interFont];

    [super initFrame:r text:text alignment:mode];
    markerList=[[List alloc] initCount:1];
    [markerList addObject:[[Marker alloc] initAt:0 type:MARKRIGHT]];
    if (!(NXOrderStrings((unsigned char *)font, (unsigned char *)GOODFONT1, YES, strlen(GOODFONT1), NULL)&&
           NXOrderStrings((unsigned char *)font, (unsigned char *)GOODFONT2, YES, strlen(GOODFONT2), NULL)))
      [self setFont:[Font newFont:font size:[[PrefAgent new] interFontSize]]];
    else
      [self setFont:[Font newFont:GOODFONT1 size:11]];
    [self setSel:0 :0];
    indentationState = [[IndentationState alloc] initText:self];
    return self;
}

- free
{
  [[markerList freeObjects] free];
  [indentationState free];
  return [super free];
}

- keyDown:(NXEvent *)theEvent
{
    switch (theEvent->data.key.charCode)
      {
        case soh: if (theEvent->flags&NX_CONTROLMASK)	// move to beginning of line
	            {
                      NXSelPt start,end; int pos;
	              [super getSel:&start :&end];
		      pos=[super positionFromLine:[super lineFromPosition:start.cp]];
		      return [self setSel:pos :pos];
		    }
		  break;
	case stx: if (theEvent->flags&NX_CONTROLMASK)	// move backward one character
	            {
                      theEvent->flags=NX_NUMERICPADMASK;
		      theEvent->data.key.charSet=1;
		      theEvent->data.key.charCode=larrow;
		    }
		  break;
        case etx: if (theEvent->flags&(NX_COMMANDMASK|NX_NUMERICPADMASK))	// Enter or Cntl-c
                   return [[[[[[NXApp mainMenu] findCellWithTag:MENU_ACTIONS] target]
                            itemList] selectCellWithTag:MENU_ACTIONS_Evaluate] sendAction];
		  else if (theEvent->flags&NX_CONTROLMASK)
		    theEvent->data.key.charCode=nul;
                  break;
	case eot: if (theEvent->flags&NX_CONTROLMASK)	// delete next character
	            {
                      NXSelPt start,end;
	              [super getSel:&start :&end];
		      [self setSel:start.cp+1 :start.cp+1];	// move cursor forward 1 char.
		      theEvent->data.key.charCode=bs;		// Text does a backspace
		    }
		  break;
	case enq: if (theEvent->flags&NX_CONTROLMASK)	// move to end of line
	            {
                      NXSelPt start,end; int pos,line;
	              [super getSel:&start :&end];
                      line=[super lineFromPosition:start.cp];
		      pos=([super positionFromLine:line+1]<0)?textLength:[super positionFromLine:line+1]-1;
		      return [self setSel:pos :pos];
		    }
		  break;
	case ack: if (theEvent->flags&NX_CONTROLMASK)	// move forward one character
	            {
                      theEvent->flags=NX_NUMERICPADMASK;
		      theEvent->data.key.charSet=1;
		      theEvent->data.key.charCode=rarrow;
		    }
		  break;
	case bel: if (theEvent->flags&NX_CONTROLMASK)	// bell
	            {
                      NXBeep(); return self;
		    }
		  break;
	case bs : break;	// delete previous character (implemented by Text)
        case ht : return [self formatTextSelectionOnly:YES];	// indent line
        case nl : break;	// newline (non-indenting cr; implemented by Text)
	case vt : if (theEvent->flags&NX_CONTROLMASK)	// delete forward to end of line
	            {
                      NXSelPt start,end; int pos,line;
	              [super getSel:&start :&end];
                      line=[super lineFromPosition:start.cp];
		      pos=([super positionFromLine:line+1]<0)?textLength:[super positionFromLine:line+1]-1;
		      [window disableFlushWindow];
		      [self setSel:start.cp :pos];
		      [window reenableFlushWindow];
		      return [self cut:self];
		    }
		  break;
        case cr : {PRE_CHANGE		// carriage return
	          [window disableFlushWindow];
                  [super keyDown:theEvent];
		  [window reenableFlushWindow];
                  POST_CHANGE(start.cp+1,start.cp,end.cp)}
                  if (([[PrefAgent new] autoindent]&&!(theEvent->flags&NX_ALTERNATEMASK))||
                       ((theEvent->flags&NX_ALTERNATEMASK)&&![[PrefAgent new] autoindent]))
                    [self formatTextSelectionOnly:YES];
		  [window flushWindow];
                  return self;
	case so : if (theEvent->flags&NX_CONTROLMASK)	// move down one line
	            {
                      theEvent->flags=NX_NUMERICPADMASK;
		      theEvent->data.key.charSet=1;
		      theEvent->data.key.charCode=darrow;
		    }
		  break;
	case dle: if (theEvent->flags&NX_CONTROLMASK)	// move up one line
	            {
                      theEvent->flags=NX_NUMERICPADMASK;
		      theEvent->data.key.charSet=1;
		      theEvent->data.key.charCode=uarrow;
		    }
		  break;
	case em : if (theEvent->flags&NX_CONTROLMASK)	// indent line (shift-tab)
	            {
		      return [self paste:self];
		    }
		  else
	            return [self formatTextSelectionOnly:YES];

        case RIGHT_PAREN  :
        case RIGHT_BRACKET:
        case RIGHT_BRACE  :
	          {PRE_CHANGE
		  [super keyDown:theEvent];
		  POST_CHANGE(start.cp,start.cp,end.cp)
		  {char c;
		  [super getSel:&start :&end];
		  [super getSubstring:&c start:end.cp-1 length:1];
		  if (inSet(c,RIGHT_PARENS)&&[[PrefAgent new] matchParens])
		    {
		      int pos;
		      char buffer[textLength+1];
		      [super getSubstring:buffer start:0 length:textLength];
		      if ((pos = findParensMatch(self,(char)theEvent->data.key.charCode,start.cp-1,buffer))==PAREN_NO_MATCH)
			NXBeep();
		      else if (pos >= 0)
			{
			  [self setSel:pos :pos+1];
			  NXPing();
			  usleep(PBLINKDELAY);
			  [self setSel:start.cp :start.cp];
			}
		    }}}
		  return self;
        }
    {PRE_CHANGE
    [super keyDown:theEvent];
    POST_CHANGE(start.cp,start.cp,end.cp)}
    return self;
}

- mouseDown:(NXEvent*)theEvent
{
    NXSelPt start,end;

    switch (theEvent->data.mouse.click)
      {
        case 1 : [super mouseDown:theEvent];
                 [self getSel:&start :&end];
                 last1ClickPos = start.cp;
		 return self;
        case 2 : [window disableFlushWindow];
	         [super mouseDown:theEvent];
	         {
		   char c[4];
		   [super getSel:&start :&end];
		   [super getSubstring:c start:start.cp-2 length:3];
                   if (inSet(c[2],LEFT_PARENS RIGHT_PARENS)&&!((c[0]=='#')&&(c[1]=='\\')))
		     {
		       [window reenableFlushWindow];
		       if (([[PrefAgent new] dblClickMatch])&&((end.cp-start.cp)==1))
			 {
			   char buffer[textLength+1];
			   int temp;
			   [super getSubstring:buffer start:0 length:textLength];
			   if ((temp = findParensMatch(self, buffer[start.cp], start.cp, buffer))<0)
			     {
			       [window flushWindow];
			       return self;
			     }
			   if (inSet(buffer[start.cp], LEFT_PARENS))
			       [super setSel:start.cp :temp+1];
			   else
			      [super setSel:temp :start.cp+1];
			 }
		       return self;
		     }
		   else if (inSet(c[2], DELIMETERSET)&&!((c[0]=='#')&&(c[1]=='\\')))
		     {
		       [[window reenableFlushWindow] flushWindow];
		       return self;
		     }
                   else
		     {
		       int i,j;
		       char buffer[textLength+1];
		       [super getSubstring:buffer start:0 length:textLength];
		       for(i=start.cp; (i>=0)&&(!inSet(buffer[i],DELIMETERSET)||((i>=2)&&(buffer[i-2]=='#')&&(buffer[i-1]=='\\'))); i--);
                       for(j=end.cp; (j<textLength)&&(!inSet(buffer[j],DELIMETERSET)||((j>=2)&&(buffer[j-2]=='#')&&(buffer[j-1]=='\\'))); j++);
                       [super setSel:i+1 :j];
		       [[window reenableFlushWindow] flushWindow];
		     }
		 }
		 return self;
        case 3 : break;
        default: [self selectScope2];
	         return self;
      }
    return [super mouseDown:theEvent];
}

- paste:sender
{
    PRE_CHANGE
    [super paste:sender];
    POST_CHANGE(end.cp+(textLength-tlength),start.cp,end.cp)
    if ([[PrefAgent new] formatAfterPaste])
      {
        [markerList addObject:[[Marker alloc] initAt:(end.cp+(textLength-tlength)) type:MARKCENTER]];
        [self setSel:start.cp :(end.cp+(textLength-tlength))];
        [self formatTextSelectionOnly:YES];
        end.cp = [[markerList objectAt:([markerList count]-1)] markerPosition];
        [self setSel:end.cp :end.cp];
        [[markerList removeLastObject] free];
      }
    return self;
}

- clear:sender
{
    PRE_CHANGE
    [super clear:sender];
    POST_CHANGE(start.cp,start.cp,end.cp)
    return self;
}

- delete:sender
{
    PRE_CHANGE
    [super delete:sender];
    POST_CHANGE(start.cp,start.cp,end.cp)
    return self;
}

- setText:(const char *)aString
{
    PRE_CHANGE
    [super setText:aString];
    POST_CHANGE(0,0,tlength)
    return self;
}

- readText:(NXStream *)stream
{
    PRE_CHANGE
    [super readText:stream];
    {
      NXStream *stream = [self stream];
      int c;
      while((c = NXGetc(stream)) != EOF)
        if(c == '\t'){
          int pos = NXTell(stream);
          int spaces = TABWIDTH - getcolumnofposition(self, pos-1) % TABWIDTH;
          char spacestring[spaces+1];
          [self setSel:pos-1 :pos];
          memset(spacestring, ' ', spaces);
          spacestring[spaces] = '\0';
          [self replaceSel:spacestring];
          stream = [self stream];
          NXSeek(stream, pos, NX_FROMSTART);
        }
      [self setSel:0 :0];
    }
    POST_CHANGE(0,0,tlength)
    return self;
}

- replaceSel:(const char *)aString length:(int)length
{
    PRE_CHANGE
    [super replaceSel:aString length:length];
    POST_CHANGE(end.cp+(textLength-tlength),start.cp,end.cp)
    return self;
}

- readSelectionFromPasteboard:pboard
{
    id ret;
    PRE_CHANGE
    ret = [super readSelectionFromPasteboard:pboard];
    POST_CHANGE(end.cp+(textLength-tlength),start.cp,end.cp)
    return ret;
}

- setFont:aFont
{
    [window disableFlushWindow];
    [super setFont:aFont];
    [[[window display] reenableFlushWindow] flushWindow];
    [[window scrollView] setLineScroll:[super lineHeight]];
    [[window scrollView] setPageScroll:[super lineHeight]];
    return self;
}


- appendNewlineIfNeeded
{
    NXSelPt start,end;
    int textPointer = [[markerList objectAt:0] markerPosition];

    if ([[PrefAgent new] transcriptMode])
      [[markerList objectAt:0] adjustTo:textLength-1];
    [super getSel:&start :&end];
    [markerList addObject:[[Marker alloc] initAt:end.cp type:MARKCENTER]];
    [markerList addObject:[[Marker alloc] initAt:start.cp type:MARKCENTER]];
    if (textPointer>0)
      {
        char buffer[2];
        [super getSubstring:buffer start:textPointer-1 length:1];
        if (buffer[0]!=NEWLINECHAR)
          {
            [[self setSel:textPointer :textPointer] replaceSel:NEWLINESTRING];
            [window setDocEdited:YES];
            [self setInsertionBar:textPointer+1];
          }
      }
    start.cp = [[markerList objectAt:([markerList count]-1)] markerPosition];
    end.cp = [[markerList objectAt:([markerList count]-2)] markerPosition];
    [self setSel:start.cp :end.cp];
    [[markerList removeLastObject] free];
    [[markerList removeLastObject] free];
    return self;
}

- appendText:(const char *)aString commented:(BOOL)aBoolean withNewline:(BOOL)aBoolean2
{
    NXSelPt start,end;
    int textPointer = [[markerList objectAt:0] markerPosition],i;

    if ([[PrefAgent new] transcriptMode])
      [[markerList objectAt:0] adjustTo:textLength-1];
    [super getSel:&start :&end];
    [markerList addObject:[[Marker alloc] initAt:end.cp type:MARKCENTER]];
    [markerList addObject:[[Marker alloc] initAt:start.cp type:MARKCENTER]];
    [window disableFlushWindow];
    if (aBoolean)
      {
        [super setSel:textPointer :textPointer];
        if ([super positionFromLine:[super lineFromPosition:textPointer]]==textPointer)
          [self replaceSel:COMMENTSTRING];
        for(i=0; aString[i]; i++)
          {
            [self replaceSel:(aString+i) length:1];
            if ((aString[i]==NEWLINECHAR)&&(aString[i+1])&&(aString[i+1]!=NEWLINECHAR)&&(aString[i+1]!=COMMENTCHAR))
              [self replaceSel:COMMENTSTRING];
          }
      }
    else
      [[self setSel:textPointer :textPointer] replaceSel:aString];
    if (aBoolean2)
      [self replaceSel:NEWLINESTRING];
    [window setDocEdited:YES];
    if ([[PrefAgent new] autoScroll])
      [super scrollSelToVisible];
    start.cp = [[markerList objectAt:([markerList count]-1)] markerPosition];
    end.cp = [[markerList objectAt:([markerList count]-2)] markerPosition];
    [self setSel:start.cp :end.cp];
    [[markerList removeLastObject] free];
    [[markerList removeLastObject] free];
    [[window reenableFlushWindow] flushWindow];
    return self;
}

- (STR)getAll
{
    [super selectAll:self];
    return [self getSelection];
}

- (STR)getSelection
{
    NXSelPt start,end;
    int length;

    [super getSel:&start :&end];
    length = end.cp-start.cp;
    selectionBuffer = (char *)(selectionBuffer==NULL?malloc(length+1):realloc(selectionBuffer, length+1));
    [super getSubstring:selectionBuffer start:start.cp length:length];
    selectionBuffer[length] = '\0';
    if ([window isMemberOf:[InteractionWin class]]&&(*selectionBuffer))
      if ([[PrefAgent new] transcriptMode])
	[self setInsertionBar:textLength];
      else
        [self setInsertionBar:end.cp];
    return selectionBuffer;
}

- setInsertionBar:(int)anInt
{
    int textPointer = (textLength<=anInt?textLength-1:anInt);
    [[markerList objectAt:0] adjustTo:textPointer];
    return [super setSel:textPointer :textPointer];
}

- (BOOL)selectScope
{
    NXSelPt start,end;
    int startpos,endpos;
    char textBuffer[textLength+1];

    [super getSel:&start :&end];
    [super getSubstring:textBuffer start:0 length:textLength];
    textBuffer[textLength] = '\0';
    if (((startpos = findBeginningOfScope(self, start.cp, textBuffer))<0)||((endpos = findEndOfScope(self, startpos, textBuffer))<0)||(endpos<end.cp))
      return NO;
    [super setSel:startpos :endpos];
    return YES;
}

- (BOOL)selectScope2
{
    int startpos,endpos;
    char textBuffer[textLength+1];

    [super getSubstring:textBuffer start:0 length:textLength];
    textBuffer[textLength] = '\0';
    if (((startpos = findBeginningOfScope(self, last1ClickPos, textBuffer))<0)||((endpos = findEndOfScope(self, startpos, textBuffer))<0)||(endpos<last1ClickPos))
      return NO;
    [super setSel:startpos :endpos];
    return YES;
}

- saveSelectionState
{
    NXSelPt start,end;
    [super getSel:&start :&end];
    [markerList addObject:[[Marker alloc] initAt:end.cp type:MARKCENTER]];
    [markerList addObject:[[Marker alloc] initAt:start.cp type:MARKCENTER]];
    return self;
}

- restoreSelectionState
{
    int start,end;
    if ([markerList count]<3) return nil;
    start = [[markerList objectAt:([markerList count]-1)] markerPosition];
    end = [[markerList objectAt:([markerList count]-2)] markerPosition];
    [self setSel:start :end];
    [[markerList removeLastObject] free];
    [[markerList removeLastObject] free];
    return self;
}

- (int)stringSearch:(const char *)theTarget forAgent:theAgent direction:(int)direction ignoreCase:(BOOL)ignorecase  wholeWord:(BOOL)wholeword inSel:(BOOL)inSel
{
    NXSelPt start,end;
    int slength,tlength = strlen(theTarget);
    int startcp,endcp,i,origTextLength=textLength;
    char *buffer;

    [super getSel:&start :&end];
    slength = end.cp-start.cp;
    if (inSel)
      {
        buffer = (char *)NXZoneMalloc([theAgent zone], slength+1);
        [super getSubstring:buffer start:start.cp length:slength];
        startcp = 0;
        endcp = (slength-tlength);
      }
    else
      {
        buffer = (char *)NXZoneMalloc([theAgent zone], origTextLength+1);
        [super getSubstring:buffer start:0 length:origTextLength];
        startcp = (direction==FINDFORWARD?end.cp:start.cp-tlength);
        endcp = (direction==FINDFORWARD?origTextLength-tlength+1:0);
      }
    for(i=startcp; (direction==FINDFORWARD?i<=endcp:i>=endcp); i+=direction)
      if (!NXOrderStrings((unsigned char *)theTarget,(unsigned char *)(buffer+i),!ignorecase,tlength,NULL))
        if (!(wholeword&&(((i>0)&&NXIsAlNum(buffer[i-1]))||(((i+tlength)<=(inSel?slength:origTextLength))&&NXIsAlNum(buffer[i+tlength])))))
          {
	    int offset=i+(inSel?start.cp:0)+(textLength-origTextLength);
	    [super setSel:offset :(offset+tlength)];
	    NXZoneFree([theAgent zone], buffer);
	    return 1;
	  }
    if (inSel)
      {
        NXZoneFree([theAgent zone], buffer);
        return 0;
      }
    startcp = (direction==FINDFORWARD?0:textLength-tlength+1);
    endcp = (direction==FINDFORWARD?end.cp-2:start.cp+1);
    origTextLength = textLength;
    for(i=startcp; (direction==FINDFORWARD?i<=endcp:i>=endcp); i+=direction)
      if (!NXOrderStrings((unsigned char *)theTarget,(unsigned char *)(buffer+i),!ignorecase,tlength,NULL))
        if (!(wholeword&&(((i>0)&&NXIsAlNum(buffer[i-1]))||(((i+tlength)<=origTextLength)&&NXIsAlNum(buffer[i+tlength])))))
          {
            int offset=i+(textLength-origTextLength);
            [super setSel:offset :(offset+tlength)];
	    NXZoneFree([theAgent zone], buffer);
	    return 1;
          }
    NXZoneFree([theAgent zone], buffer);
    return 0;
}

- formatTextSelectionOnly:(BOOL)selOnly
{
  NXSelPt start, end;
  int startline, finishline, i;
  
  [self getSel:&start :&end];
  [markerList addObject:[[Marker alloc] initAt:end.cp type:MARKRIGHT]];
  [markerList addObject:[[Marker alloc] initAt:start.cp type:MARKRIGHT]];

  if (!selOnly && start.cp >= end.cp){
    startline = 1;
    finishline = [self lineFromPosition:textLength];
  } else {
    char c;
    startline = [self lineFromPosition:start.cp];
    [self getSubstring:&c start:end.cp-1 length:1];
    finishline = [self lineFromPosition: ((c == '\n') && (start.cp < end.cp)
                                          ? (end.cp - 1)
                                          : end.cp)];
  }

  for (i = startline; i <= finishline; i++) {
    int desired = [indentationState determineIndentation:i];

    if (desired >= 0){
      BOOL anyTabs = NO;
      int current;
      unsigned char c;
      NXStream *stream = [self stream];

      NXSeek(stream, [self positionFromLine:i], NX_FROMSTART);
      for (current = 0; ; current++){
        if((c = NXGetc(stream)) == '\t')
          anyTabs = YES;
        else if (c != ' ')
          break;
      }
#ifdef DEBUG_INDENTATION
      fprintf(stderr, "Linen %d; current = %d, desired = %d%s\n",
              i, current, desired, anyTabs?"; tabs present":"");
#endif
      if(anyTabs || current != desired){
        int startpos = [self positionFromLine:i];
        char spacestring[desired+1];

        [self setSel:startpos :startpos+current];
        memset(spacestring, ' ', desired);
        spacestring[desired] = '\0';
        [self replaceSel:spacestring];
        [window setDocEdited:YES];
      }
      else
        NXUngetc(stream);
    }
  }
  start.cp = [[markerList objectAt:([markerList count]-1)] markerPosition];
  end.cp = [[markerList objectAt:([markerList count]-2)] markerPosition];
  [self setSel:start.cp :end.cp];
  [[markerList removeLastObject] free];
  [[markerList removeLastObject] free];
  return self;
}

@end

static BOOL inSet(const char element, const char *set)
{
    int i;

    for(i=0; *(set+i); i++)
      if (element==*(set+i))
        return YES;
    return NO;
}

static int findParensMatch(id self, const char parens, int startpos, char *text)
{
    int i,count=0,init,match,direction,textlength=[self textLength];

    switch (init=parens)
      {
        case LEFT_PAREN   : match = RIGHT_PAREN; direction = 1; break;
        case LEFT_BRACKET : match = RIGHT_BRACKET; direction = 1; break;
        case LEFT_BRACE   : match = RIGHT_BRACE; direction = 1; break;
        case RIGHT_PAREN  : match = LEFT_PAREN; direction = -1; break;
        case RIGHT_BRACKET: match = LEFT_BRACKET; direction = -1; break;
        case RIGHT_BRACE  : match = LEFT_BRACE; direction = -1; break;
        default : return NOT_REALLY_PAREN;
      }
    if ((text[startpos-2]=='#')&&(text[startpos-1]=='\\'))
      return NOT_REALLY_PAREN;
    for(i=startpos+direction; ((i>=0)&&(i<textlength))&&((count>0)||(text[i]!=match)||((i>=2)&&(text[i-2]=='#')&&(text[i-1]=='\\'))); i+=direction)
      {
        if ((direction>0)&&(text[i]=='\"')&&!((text[i-2]=='#')&&(text[i-1]=='\\')))
          {
	    i=skipstring(text,textlength,i);
	    if (i<0) break;
	  }
        else if ((direction>0)&&(text[i]==COMMENTCHAR)&&!((text[i-2]=='#')&&(text[i-1]=='\\')))
          i=skipspacecommentnewline(text,textlength,i-1)-1;
        else if ((text[i]==init)&&!((text[i-2]=='#')&&(text[i-1]=='\\')))
          count++;
        else if ((text[i]==match)&&!((text[i-2]=='#')&&(text[i-1]=='\\')))
          count--;
      }
    return ((i>=textlength)||(i<0)?PAREN_NO_MATCH:i);
}

static int findBeginningOfScope(id self, int currpos, char *text)
{
    int line,startpos;

    line = [self lineFromPosition:currpos];
    startpos = [self positionFromLine:line];
    for(; line&&(!text[startpos]||inSet(text[startpos],BADLINEBEGINSET)); startpos = [self positionFromLine:--line]);
    return (line<=0?-1:startpos);
}

static int skipspacecommentnewline(const char buffer[], int textlength, int endpos)
{
    for(endpos++; (endpos<textlength)&&(buffer[endpos]!=NEWLINECHAR)&&NXIsSpace(buffer[endpos]); endpos++);
      if (buffer[endpos]==NEWLINECHAR)
	return endpos+1;
      else if ((buffer[endpos]==COMMENTCHAR)&&!((endpos>=2)&&(buffer[endpos-2]=='#')&&(buffer[endpos-1]=='\\')))
	{
	  for(endpos++; (endpos<textlength)&&(buffer[endpos]!=NEWLINECHAR); endpos++);
	  if (endpos>=textlength) return endpos;
	  return endpos+1;
	}
      else
	return endpos;
}

static int skipstring(const char buffer[], int textlength, int endpos)
{
    if ((endpos>=2)&&(buffer[endpos-1]=='\\')&&(buffer[endpos-2]=='#'))
      return endpos;
    while (1)
      {
        for(endpos++; (endpos<textlength)&&(buffer[endpos]!='\"'); endpos++);
        if (endpos>=textlength) return -1;
        if ((buffer[endpos-1]!='\\')||((endpos>=2)&&(buffer[endpos-2]=='\\'))) return endpos;
      }
}

static int findEndOfScope(id self, int startpos, char *text)
{
    int endpos=0;

    for(; text[startpos]&&inSet(text[startpos],PREFIXCHARS); startpos++);
    if ((text[startpos]=='\\')&&(text[startpos-1]=='#'))
      startpos--;
    if (inSet(text[startpos],LEFT_PARENS))
      {
	if ((endpos = findParensMatch(self,text[startpos],startpos,text))<0)
	  return -1;
      }
    else if (text[startpos]=='\"')
      {
	if ((endpos = skipstring(text,strlen(text),startpos))<0)
	  return -1;
      }
    else
      {
        for(endpos=startpos; text[endpos]&&(!inSet(text[endpos],DELIMETERSET)||((text[endpos-2]=='#')&&(text[endpos-1]=='\\'))); endpos++);
        endpos--;
      }
    endpos = skipspacecommentnewline(text, strlen(text), endpos);
    if (endpos<=startpos) return -1;
    return endpos;
}

static int getcolumnofposition(id self, int charpos)
{
    return charpos-[self positionFromLine:[self lineFromPosition:charpos]];
}


@implementation Marker

- init
{
    return [self initAt:0 type:MARKCENTER];
}

- initAt:(int)anInt type:(signed char)aChar
{
    [super init];
    charpos = anInt;
    type = aChar;
    return self;
}

- (int)markerPosition
{
    return charpos;
}

- adjustTo:(int)anInt forPositionsFrom:(int)begin to:(int)end andAfterBy:(int)anInt2
{
    if (((charpos==begin)&&(type==MARKRIGHT))||((begin<charpos)&&(charpos<end))||((charpos==end)&&(type==MARKLEFT)))
      charpos = anInt;
    else if (end<=charpos)
      charpos += anInt2;
    return self;
}

- adjustTo:(int)anInt
{
    charpos = anInt;
    return self;
}

@end

@implementation IndentationState

- changeAt:(int)position
{
  stream = NULL;
  if ( [text lineFromPosition:position] < knownLine ){
    knownLine = 1;
    while(contexts){
      Context *temp = contexts;
      contexts = temp->next;
      free(temp);
    }
    inString = NO;
  }
  return self;
}

- init
{
    return [self initText:nil];
}

- initText:theText
{
  [super init];
  text = theText;
  knownLine = 1;
  stream = NULL;
  contexts = NULL;
  inString = NO;
  keywordListVersion = [[PrefAgent new] keywordListVersion];
  return self;
}

- free
{
    while(contexts)
      {
        Context *temp = contexts;
        contexts = temp->next;
        free(temp);
      }
    return [super free];
}

- (int)determineIndentation:(int)line
{
  int currentKeywordListVersion = [[PrefAgent new] keywordListVersion];

  if (keywordListVersion != currentKeywordListVersion){
    keywordListVersion = currentKeywordListVersion;
    [self changeAt:0];
  }
  if (line < knownLine || (line - knownLine) > MAX_FWD_ONLY){
    int l;
    for(l = line-1; l > 1 && l != knownLine; l--){
      char c;
      [text getSubstring:&c start:[text positionFromLine:l] length:1];
      if(!index(BADLINEBEGINSET, c))
        break;
    }
    if(l == 0)
      l++;
    if(l != knownLine){
      knownLine = l;
      stream = NULL;
      inString = NO;
      while(contexts){
        Context *temp = contexts;
        contexts = temp->next;
        free(temp);
      }
    }
  }
  while(knownLine < line)
    [self scanLine];
  return [self indent];
}
 
- setupStream
{
  if (stream == NULL){
    stream = [text stream];
    NXSeek(stream, [text positionFromLine:knownLine], NX_FROMSTART);
  }
  return self;
}

- (int)indent
{
#ifdef DEBUG_INDENTATION
  fprintf(stderr, "start of IndentationState/indent ");
  [self describeSelf];
#endif
  if (inString)
    return -1;
  else if (contexts == NULL)
    return 0;
  else {
    Context *c = contexts;
    if (c->elts < c->distinguishedElts + 1)
      return BIG_INDENT + getcolumnofposition(text, c->startPosition);
    else if (c->elts == c->distinguishedElts + 1)
      return SMALL_INDENT + getcolumnofposition(text, c->startPosition);
    else
      return getcolumnofposition(text, c->alignmentPosition);
  }
}

- scanLine
{
  BOOL anythingYet = NO;
  Context *context = contexts;
  BOOL prefix = NO;
  char c;

#ifdef DEBUG_INDENTATION
  fprintf(stderr, "start of IndentationState/scanLine ");
  [self describeSelf];
#endif
  [self setupStream];
  knownLine++;
  if (inString)
    goto scan_string;
  while (1){
    switch (c = NXGetc(stream)){
    case '\n': case '\377':
      return self;
    case ' ': case '\t':
      continue;
    case ';':
      while(NXGetc(stream) != '\n')
        ;
      return self;
    case '\'': case'`': case ',': case '@': case '#':
      if(!prefix){
        prefix = YES;
        newElt(context, anythingYet, stream);
      }
      if(c == '#'){
        if(NXGetc(stream) == '\\'){
          if(isalpha(c = NXGetc(stream))){
            while(index(DELIMETERSET, NXGetc(stream)) == NULL)
              ;
            NXUngetc(stream);
            break;
          } else if(c == '\n')
            NXUngetc(stream);
          else
            break;
        } else
          NXUngetc(stream);
      }
      continue;
    case '(': case '[': case '{':
      if(!prefix)
        newElt(context, anythingYet, stream);
      contexts = malloc(sizeof(Context));
      ((Context *)contexts)->next = context;
      context = contexts;
      context->isLet = NO;
      context->elts = 0;
      context->distinguishedElts = -1;
      context->startPosition = (context->alignmentPosition=NXTell(stream)) - 1;
      break;
    case ')': case ']': case '}':
      if(context != NULL){
        contexts = context->next;
        free(context);
        context = contexts;
      }
      break;
    case '"':
      inString = YES;
      if(!prefix)
        newElt(context, anythingYet, stream);
    scan_string:
      while (1){
        if ((c = NXGetc(stream)) == '\n')
          return self;
        else if (c == '\\'){
          if ((c = NXGetc(stream)) == '\n')
            NXUngetc(stream);
        } else if (c == '"'){
          inString = NO;
          break;
        }
      }
      break;
    default:
#ifdef DEBUG_INDENTATION
      fprintf(stderr, "start of token ");
      [self describeSelf];
#endif
      if(!prefix)
        newElt(context, anythingYet, stream);
      if(!prefix && context != NULL && context->elts == 2
         && context->isLet)
        context->distinguishedElts++;
      if (!prefix && context != NULL && context->elts == 1) {
        char tokbuf[MAXTOK+1];
        int i = 0;
        do {
          if (i < MAXTOK)
            tokbuf[i++] = c;
        } while(index(DELIMETERSET, c = NXGetc(stream)) == NULL);
        NXUngetc(stream);
        tokbuf[i] = '\0';
        context->distinguishedElts = [[PrefAgent new] deForSpecialForm:tokbuf];
        if(!NXOrderStrings((unsigned char *) tokbuf, (unsigned char *) "let",
                           NO, -1, NULL))
          context->isLet = YES;
      } else{
        while(index(DELIMETERSET, NXGetc(stream)) == NULL)
          ;
        NXUngetc(stream);
      }
#ifdef DEBUG_INDENTATION
      fprintf(stderr, "end of token ");
      [self describeSelf];
#endif
      break;
    }
    prefix = NO;
    anythingYet = YES;
  }
}

- describeSelf
{
  Context *context = contexts;
  fprintf(stderr, "indentation state\n");
  fprintf(stderr, "  knownLine: %d\n", knownLine);
  if(stream == NULL)
    fprintf(stderr, "  no stream\n");
  else{
    int pos = NXTell(stream);
    fprintf(stderr, "  stream position: %d (row %d, col %d)\n",
            pos, [text lineFromPosition:pos],
            getcolumnofposition(text, pos));
  }
  fprintf(stderr, "  %sin string\n", inString?"":"not ");
  while(context){
    fprintf(stderr, "  elts: %d, des: %d, startpos: %d, alignpos: %d\n",
            context->elts, context->distinguishedElts, context->startPosition,
            context->alignmentPosition);
    context = context->next;
  }
  return self;
}

@end

static void newElt(Context *context, BOOL anythingYet, NXStream *stream){
  if (context == NULL)
    return;
  if(context->elts == 1 || (context->elts > 1 && !anythingYet))
    context->alignmentPosition = NXTell(stream)-1;
  context->elts++;
}
